﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;

using NMemcached.Client.Binary;
using NMemcached.Client.Configuration;

namespace NMemcached.Client
{
    /// <summary>
    /// A memcached server connection using the binary protocol
    /// </summary>
    public class BinaryConnection : ServerConnection
    {
        //Used to create the value of the opaqe field to verify
        //the response is the correct one for the response
        private Random _rng;

        //Commands that have no key, extras, or content
        private static readonly BinaryRequest StatRequest;
        private static readonly BinaryRequest VersionRequest;
        private static readonly BinaryRequest QuitRequest;

        /// <summary>
        /// Gets or sets the endianess of the memcached server
        /// </summary>
        public Endian Endian { get; set; }

        static BinaryConnection()
        {
            //Create the commands that have no key, extras, or content
            BinaryConnection.StatRequest = new BinaryRequest(OpCode.Stat);
            BinaryConnection.VersionRequest = new BinaryRequest(OpCode.Version);
            BinaryConnection.QuitRequest = new BinaryRequest(OpCode.Quit);
        }

        /// <summary>
        /// Initializes a new instance of the BinaryConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The end point of the memcached server</param>
        public BinaryConnection(TransportType transportType, EndPoint remoteEp)
            : this(transportType, remoteEp, Encoding.UTF8, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the BinaryConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The end point of the memcached server</param>
        /// <param name="protocolEncoding">The character encoding for string data sent to and read from the server</param>
        public BinaryConnection(TransportType transportType, EndPoint remoteEp, Encoding protocolEncoding)
            : this(transportType, remoteEp, protocolEncoding, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the BinaryConnection class
        /// </summary>
        /// <param name="transportType">The transport protocol used by the connection</param>
        /// <param name="remoteEp">The end point of the memcached server</param>
        /// <param name="protocolEncoding">The character encoding for string data sent to and read from the server</param>
        /// <param name="useCompression">Determines if non primitive data is compressed when sent to or decompressed when read from the server</param>
        public BinaryConnection(TransportType transportType, EndPoint remoteEp, Encoding protocolEncoding, bool useCompression)
            : base(transportType, remoteEp, protocolEncoding, useCompression)
        {
            //Memcached is compiled big endian so set it as the default
            this.Endian = Endian.Big;
            //Init the random number generator
            this._rng = new Random((int)DateTime.Now.Ticks);
        }

        /// <summary>
        /// Adds a key/value pair to a Memcached server if the data does not already exist.
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Add(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Add, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Adds data to an existing cache entry after the existing data
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Append(string key, object data)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Append, key, data, DateTime.MinValue);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Stores data for an existing key only if it has not been updated since the last get
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <param name="casUnique">The unique value of an an existing entry</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode CheckAndSet(string key, object data, DateTime expiration, ulong casUnique)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Set, key, data, expiration, casUnique);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Closes the connection to the Memcached server by sending the OpCode.Quit command
        /// </summary>
        public override void Close()
        {
            this.SendBinaryRequest(BinaryConnection.QuitRequest);
            base.Close();
        }

        /// <summary>
        /// Decreases a numeric cached value at the specified key by the specified amount
        /// </summary>
        /// <param name="key">The cache entry to increment</param>
        /// <param name="count">The amount by which the value is decremented</param>
        /// <returns>The new value of the data after the decrement operation was carried out</returns>
        public override ulong Decrement(string key, ulong count)
        {
            try
            {
                return this.SendIncDecCommand(key, count, DateTime.MinValue, true);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Removes a cache entry
        /// </summary>
        /// <param name="key">The key of the entry to be deleted</param>
        /// <returns>The response code of the command sent to the server</returns>
        public override ResponseCode Delete(string key)
        {
            try
            {
                return this.SendDeleteCommand(key);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Immediately invalidates all cached items in all servers
        /// </summary>
        /// <remarks>
        /// <para>The "flush_all" command has no response other than OK unless there is an error</para>
        /// </remarks>
        public override void FlushAll()
        {
            this.SendFlushCommand(TimeSpan.Zero);
        }

        /// <summary>
        /// Invalidates all cached items in all servers after the specified delay
        /// </summary>
        /// <param name="delay">The amount of time to pause prior to executing the "flush_all" command.</param>
        /// <remarks>
        /// <para>The "flush_all" command has no response other than OK unless there is an error.</para>
        /// </remarks>
        public override void FlushAll(TimeSpan delay)
        {
            this.SendFlushCommand(delay);
        }

        /// <summary>
        /// Reads a single entry from the cache
        /// </summary>
        /// <param name="key">The key of the item to get from the cache</param>
        /// <returns>The data from the cache associated with the specified key</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override object Get(string key)
        {
            //Get the detail and return just the value
            try
            {
                CacheDetail detail = this.GetWithDetail(key);
                object found = null;
                if (detail != null)
                {
                    found = detail.Value;
                }
                return found;
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Reads multiple entries from the cache
        /// </summary>
        /// <param name="keys">A list of keys for which the data is read</param>
        /// <returns>A keyed collection of items read from the cache</returns>
        /// <exception cref="System.ArgumentNullException">A null key was encountered</exception>
        public override IDictionary<string, object> Get(params string[] keys)
        {
            try
            {
                //Get the details for each of the keys
                IDictionary<string, CacheDetail> details = this.GetWithDetail(keys);
                Dictionary<string, object> items = null;
                if (details != null && details.Count > 0)
                {
                    //Extract just the values from the details
                    items = new Dictionary<string, object>();
                    foreach (KeyValuePair<string, CacheDetail> pair in details)
                    {
                        if (pair.Value == null)
                        {
                            items.Add(pair.Key, null);
                        }
                        else
                        {
                            items.Add(pair.Key, pair.Value.Value);
                        }
                    }
                }
                return items;
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Reads a single entry from the cache and returns the data with detail information
        /// </summary>
        /// <param name="key">The key of the item to get from the cache</param>
        /// <returns>The data from the cache associated with the specified key with detail information</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override CacheDetail GetWithDetail(string key)
        {
            try
            {
                CacheDetail found = null;

                IDictionary<string, CacheDetail> result = this.GetWithDetail(new string[] { key });

                if (result != null && result.Count > 0)
                {
                    if (result.ContainsKey(key))
                    {
                        found = result[key];
                    }
                }

                return found;
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Reads multiple entries from the cache with detail information
        /// </summary>
        /// <param name="keys">A list of keys for which the data is read</param>
        /// <returns>A keyed collection of items read from the cache with detail information</returns>
        /// <exception cref="System.ArgumentNullException">A null key was encountered</exception>
        public override IDictionary<string, CacheDetail> GetWithDetail(params string[] keys)
        {
            Dictionary<string, CacheDetail> items = null;
            if (keys != null && keys.Length > 0)
            {
                BinaryRequest currentRequest = null;
                BinaryResponse currentResponse = null;
                List<BinaryResponse> responses = new List<BinaryResponse>();

                //Assemble the get commands
                foreach (string key in keys)
                {
                    currentRequest = new BinaryRequest(OpCode.GetWithKey);

                    if (!String.IsNullOrEmpty(key))
                    {
                        currentRequest.Key = this.ProtocolEncoding.GetBytes(key);
                    }
                    else
                    {
                        throw new ArgumentNullException(StringTable.Get("error_NullKey"));
                    }

                    currentResponse = this.SendBinaryRequest(currentRequest);

                    if (currentResponse != null)
                    {
                        responses.Add(currentResponse);
                    }
                }
                //Only bother if we have something to send back
                if (responses.Count > 0)
                {
                    CacheDetail detail = null;
                    items = new Dictionary<string, CacheDetail>();
                    foreach (BinaryResponse response in responses)
                    {
                        detail = new CacheDetail();
                        if (response.Key != null)
                        {
                            detail.Key = this.ProtocolEncoding.GetString(response.Key);
                        }
                        detail.CasUnique = response.Cas;
                        detail.Opaque = response.Opaque;
                        //Ignore any items not found
                        if (response.ResponseCode == ResponseCode.NoError)
                        {
                            detail.Value = this.ParseGetResponse(response);
                            items.Add(detail.Key, detail);
                        }
                        else
                        {
                            items.Add(detail.Key, null);
                        }
                    }
                }
            }
            return items;
        }

        /// <summary>
        /// Increases a numeric cached value at the specified key by the specified amount
        /// </summary>
        /// <param name="key">The cache entry to increment</param>
        /// <param name="count">The amount by which the value is incremented</param>
        /// <returns>The new value of the data after the increment operation was carried out</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ulong Increment(string key, ulong count)
        {
            try
            {
                return this.SendIncDecCommand(key, count, DateTime.MinValue, false);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Adds data to an existing cache entry before the existing data
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Prepend(string key, object data)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Prepend, key, data, DateTime.MinValue);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Replaces the data associated with an existing cache entry
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Replace(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Replace, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Adds data to the cache, overwriting any data in an existing entry
        /// </summary>
        /// <param name="key">The key to which the data is associated</param>
        /// <param name="data">The data to be stored in the cache</param>
        /// <param name="expiration">The timestamp at wich the entry expires</param>
        /// <returns>The response code of the command sent to the server</returns>
        /// <exception cref="System.ArgumentNullException">key is null</exception>
        public override ResponseCode Set(string key, object data, DateTime expiration)
        {
            try
            {
                return this.SendStorageCommand(OpCode.Set, key, data, expiration);
            }
            catch (ArgumentNullException e)
            {
                throw e;
            }
        }

        /// <summary>
        /// Reads the cache statistics for the server
        /// </summary>
        /// <returns>The server statistics</returns>
        public override IDictionary<string, string> Stats()
        {
            //Get the statistics response
            byte[] responseBytes = this.SendCommand(BinaryConnection.StatRequest.ToBytes(this.Endian));

            using (MemoryStream byteStream = new MemoryStream(responseBytes))
            {
                byteStream.Position = 0;
                //Get all of the response packets. Each statistic is in its own packet.
                IEnumerable<BinaryResponse> responses = BinaryResponse.Parse(byteStream, this.Endian);
                Dictionary<string, string> stats = new Dictionary<string, string>();
                foreach (BinaryResponse response in responses)
                {
                    //A null key means it's the empty packet that terminates the stat list
                    if (response.Key != null)
                    {
                        if (response.Value != null)
                        {
                            stats.Add(this.ProtocolEncoding.GetString(response.Key), this.PayloadEncoding.GetString(response.Value));
                        }
                    }
                }
                return stats;
            }
        }

        /// <summary>
        /// Gets the version numbers of the server
        /// </summary>
        /// <returns>A string representation of the memcached server version number</returns>
        public override string Version()
        {
            BinaryResponse response = this.SendBinaryRequest(BinaryConnection.VersionRequest);
            string versionString = null;
            if (response.Value != null)
            {
                versionString = this.ProtocolEncoding.GetString(response.Value);
            }
            return versionString;
        }

        //Private methods

        private object ParseGetResponse(BinaryResponse response)
        {
            object data = null;

            if (response.ResponseCode == ResponseCode.NoError)
            {
                using (MemoryStream extrasStream = new MemoryStream(response.Extras))
                {
                    using (EndianBinaryReader extrasReader = new EndianBinaryReader(extrasStream, this.ProtocolEncoding))
                    {
                        //The flags are stored as uint in the binary protocol
                        //but the storage flags inherit from ushort to maintain
                        //compatability
                        StorageFlags flags = (StorageFlags)((ushort)extrasReader.ReadUInt32());

                        byte[] valueBytes = response.Value;
                        //Decompress the data if the compression flag is set
                        if ((flags & StorageFlags.Compressed) == StorageFlags.Compressed)
                        {
                            valueBytes = this.Compressor.Decompress(valueBytes);
                        }

                        //Deserialize if the flag is set. If the data was set from this
                        //client then primitives and strings will not have the Serialized flag
                        if ((flags & StorageFlags.Serialized) == StorageFlags.Serialized)
                        {
                            IEnumerable<object> deserialized = this.Serializer.Deserialize(valueBytes);
                            if (deserialized != null && deserialized.Count() == 1)
                            {
                                data = deserialized.ElementAt(0);
                            }
                            else
                            {
                                data = deserialized;
                            }
                        }
                        else
                        {
                            if (valueBytes != null)
                            {
                                //It's etiher a string or unsigned long at this point
                                string rawValue = this.PayloadEncoding.GetString(valueBytes);
                                ulong converted = 0;

                                if (ulong.TryParse(rawValue, out converted))
                                {
                                    //The parse succeeded so return the long integer
                                    data = converted;
                                }
                                else
                                {
                                    //The parse failed so return the value as a string
                                    data = rawValue;
                                }
                            }
                        }
                    }
                }
            }

            return data;
        }

        private BinaryResponse SendBinaryRequest(BinaryRequest request)
        {
            //Set the opaque before sending
            request.Opaque = (uint)this._rng.Next();
            byte[] responseBytes = this.SendCommand(request.ToBytes(this.Endian));
            BinaryResponse response = BinaryResponse.Parse(responseBytes, this.Endian);
            //Check the opaque to make sure it's the same
            if (response.Opaque != request.Opaque)
            {
                throw new MemcachedException(StringTable.Get("error_BadOpaque"));
            }
            return response;
        }

        private ResponseCode SendDeleteCommand(string key)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            //Send the delete command
            BinaryRequest request = new BinaryRequest(OpCode.Delete);
            request.Key = this.ProtocolEncoding.GetBytes(key);
            BinaryResponse response = this.SendBinaryRequest(request);
            return response.ResponseCode;
        }

        private void SendFlushCommand(TimeSpan delay)
        {
            using (MemoryStream extrasStream = new MemoryStream())
            {
                using (EndianBinaryWriter extrasWriter = new EndianBinaryWriter(extrasStream, this.ProtocolEncoding))
                {
                    if (delay == TimeSpan.Zero)
                    {
                        //Set no delay
                        extrasWriter.Write((uint)0);
                    }
                    else
                    {
                        //Set the delay to the total seconds of the time span
                        extrasWriter.Write((uint)delay.TotalSeconds);
                    }

                    //Send the flush command
                    BinaryRequest request = new BinaryRequest(OpCode.Flush);

                    request.Extras = extrasStream.ToArray();

                    BinaryResponse response = this.SendBinaryRequest(request);

                    if (response.ResponseCode != ResponseCode.NoError)
                    {
                        string message = null;
                        if (response.Value != null)
                        {
                            message = this.ProtocolEncoding.GetString(response.Value);
                        }
                        throw new MemcachedException(message);
                    }
                }
            }
        }

        private ulong SendIncDecCommand(string key, ulong count, DateTime expiration, bool decrement)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            using (MemoryStream extrasStream = new MemoryStream())
            {
                using (EndianBinaryWriter writer = new EndianBinaryWriter(extrasStream, this.ProtocolEncoding))
                {
                    //Set either the increment or decrement op code
                    OpCode operation = OpCode.Increment;
                    if (decrement)
                    {
                        operation = OpCode.Decrement;
                    }

                    //Create the request for the operation and assign the key
                    BinaryRequest request = new BinaryRequest(operation);
                    request.Key = this.ProtocolEncoding.GetBytes(key);

                    //Write the number by which to increment
                    writer.Write(count);
                    //Set the initial value to zero if the key does not exist
                    writer.Write((ulong)0);
                    //Set the expiration if specified
                    uint expires = 0;
                    if (expiration > DateTime.MinValue)
                    {
                        expires = ServerConnection.GetExpirationTime(expiration);
                    }
                    writer.Write(expires);

                    //Set the extras
                    request.Extras = extrasStream.ToArray();

                    //Send the command
                    BinaryResponse response = this.SendBinaryRequest(request);

                    ulong result = 0;
                    if (response.ResponseCode == ResponseCode.NoError)
                    {
                        using (MemoryStream responseValueStream = new MemoryStream(response.Value))
                        {
                            responseValueStream.Position = 0;
                            using (EndianBinaryReader reader = new EndianBinaryReader(responseValueStream, this.PayloadEncoding))
                            {
                                //Get the incremented or decremented value
                                result = reader.ReadUInt64();
                            }
                        }
                    }
                    return result;
                }
            }
        }

        private ResponseCode SendStorageCommand(OpCode operation, string key, object data, DateTime expiration)
        {
            return this.SendStorageCommand(operation, key, data, expiration, null);
        }

        private ResponseCode SendStorageCommand(OpCode operation, string key, object data, DateTime expiration, ulong? casUnique)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }
            //Build the request
            BinaryRequest request = new BinaryRequest(operation);
            //Set the cache key
            request.Key = this.ProtocolEncoding.GetBytes(key);
            //Set the CAS unique value if provided
            if (casUnique != null)
            {
                request.Cas = (ulong)casUnique;
            }
            //Set the default flags
            StorageFlags flags = StorageFlags.None;
            byte[] valueBytes = null;
            if (data.GetType().IsPrimitive)
            {
                //This works for some reason. The server sees the numeric string
                //and saves it as a number.
                valueBytes = this.PayloadEncoding.GetBytes(Convert.ToUInt64(data).ToString());
            }
            else
            {
                if (typeof(string).IsAssignableFrom(data.GetType()))
                {
                    //Set the raw string as the value
                    valueBytes = this.PayloadEncoding.GetBytes(data.ToString());
                }
                else
                {
                    //Serialize anything other than primitives or strings
                    flags |= StorageFlags.Serialized;
                    valueBytes = this.Serializer.Serialize(data);
                }
                //Compress serialized objects and strings if needed
                if (this.UseCompression)
                {
                    flags |= StorageFlags.Compressed;
                    valueBytes = this.Compressor.Compress(valueBytes);
                }
            }
            //Set the value
            request.Value = valueBytes;
            //Append and prepend cannot have extras
            if (operation != OpCode.Append && operation != OpCode.Prepend)
            {
                //Create a stream to hold the extras data
                using (MemoryStream extrasStream = new MemoryStream())
                {
                    using (EndianBinaryWriter writer = new EndianBinaryWriter(extrasStream, this.ProtocolEncoding))
                    {
                        //Write the flags
                        writer.Write((uint)flags);
                        //Get the unix expiration date
                        uint expires = 0;
                        if (expiration > DateTime.MinValue)
                        {
                            expires = ServerConnection.GetExpirationTime(expiration);
                        }
                        //Write the date to the extras
                        writer.Write(expires);
                        writer.Flush();
                        //Set the extras bytes
                        request.Extras = extrasStream.ToArray();
                    }
                }
            }

            //Send back the response
            BinaryResponse response = this.SendBinaryRequest(request);
            return response.ResponseCode;
        }
    }
}
